package br.com.winesys.geta.databinding;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JPasswordField;
import javax.swing.JSpinner;
import javax.swing.text.JTextComponent;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import org.apache.commons.beanutils.PropertyUtils;
import br.com.winesys.geta.databinding.component.Hidden;
import br.com.winesys.geta.databinding.component.editor.CharacterTextField;
import br.com.winesys.geta.databinding.component.selector.EnumSelector;
import br.com.winesys.geta.databinding.component.selector.FileSelector;
import br.com.winesys.geta.databinding.component.selector.FolderSelector;
import br.com.winesys.geta.databinding.component.selector.ObjectSelector;
import br.com.winesys.geta.databinding.component.table.ListSelector;
import br.com.winesys.geta.databinding.component.table.TableEditor;
import br.com.winesys.geta.databinding.component.table.TableViewer;
import br.com.winesys.geta.databinding.componentBinder.CharacterTextFieldBinder;
import br.com.winesys.geta.databinding.componentBinder.EnumSelectorBinder;
import br.com.winesys.geta.databinding.componentBinder.FileSelectorBinder;
import br.com.winesys.geta.databinding.componentBinder.FolderSelectorBinder;
import br.com.winesys.geta.databinding.componentBinder.HiddenBinder;
import br.com.winesys.geta.databinding.componentBinder.JCheckBoxBinder;
import br.com.winesys.geta.databinding.componentBinder.JComboBoxBinder;
import br.com.winesys.geta.databinding.componentBinder.JDateChooserBinder;
import br.com.winesys.geta.databinding.componentBinder.JFormattedTextBinder;
import br.com.winesys.geta.databinding.componentBinder.JPasswordBinder;
import br.com.winesys.geta.databinding.componentBinder.JSpinnerBinder;
import br.com.winesys.geta.databinding.componentBinder.JTextComponentBinder;
import br.com.winesys.geta.databinding.componentBinder.ListSelectorBinder;
import br.com.winesys.geta.databinding.componentBinder.ObjectSelectorBinder;
import br.com.winesys.geta.databinding.componentBinder.TableEditorBinder;
import br.com.winesys.geta.databinding.componentBinder.TableViewerBinder;
import com.toedter.calendar.JDateChooser;

public final class Binder {

    private static final Binder INSTANCE = new Binder();
    private final Map<Class<?>, ComponentBinder<?>> componentBinderMap = new HashMap<Class<?>, ComponentBinder<?>>();

    public static Binder getInstance() {
        return INSTANCE;
    }

    private Binder() {
        if (INSTANCE != null) {
            throw new AssertionError("An Binder has already been instantiated");
        }
        this.componentBinderMap.put(TableEditor.class, TableEditorBinder.getInstance());
        this.componentBinderMap.put(TableEditor.class, TableEditorBinder.getInstance());
        this.componentBinderMap.put(TableViewer.class, TableViewerBinder.getInstance());
        this.componentBinderMap.put(Hidden.class, HiddenBinder.getInstance());
        this.componentBinderMap.put(CharacterTextField.class, CharacterTextFieldBinder.getInstance());
        this.componentBinderMap.put(EnumSelector.class, EnumSelectorBinder.getInstance());
        this.componentBinderMap.put(FileSelector.class, FileSelectorBinder.getInstance());
        this.componentBinderMap.put(FolderSelector.class, FolderSelectorBinder.getInstance());
        this.componentBinderMap.put(ObjectSelector.class, ObjectSelectorBinder.getInstance());
        this.componentBinderMap.put(ListSelector.class, ListSelectorBinder.getInstance());
        this.componentBinderMap.put(JCheckBox.class, JCheckBoxBinder.getInstance());
        this.componentBinderMap.put(JComboBox.class, JComboBoxBinder.getInstance());
        this.componentBinderMap.put(JFormattedTextField.class, JFormattedTextBinder.getInstance());
        this.componentBinderMap.put(JPasswordField.class, JPasswordBinder.getInstance());
        this.componentBinderMap.put(JSpinner.class, JSpinnerBinder.getInstance());
        this.componentBinderMap.put(JTextComponent.class, JTextComponentBinder.getInstance());
        this.componentBinderMap.put(JDateChooser.class, JDateChooserBinder.getInstance());
    }

    private ComponentBinder<?> getComponentBinder(Class<?> c) {
        Class<?> clazz = c;
        ComponentBinder<?> componentBinder = this.componentBinderMap.get(clazz);
        while (componentBinder == null && clazz.getSuperclass() != Object.class) {
            clazz = clazz.getSuperclass();
            componentBinder = this.componentBinderMap.get(clazz);
        }
        return componentBinder;
    }

    public <T extends JComponent> Object getComponentValue(T component) {
        ComponentBinder<T> componentBinder = (ComponentBinder<T>) this.getComponentBinder(component.getClass());
        if (componentBinder != null) {
            return componentBinder.read(component);
        }
        return null;
    }

    public <T extends JComponent> void setComponentValue(T component, Object value) {
        ComponentBinder<T> componentBinder = (ComponentBinder<T>) this.getComponentBinder(component.getClass());
        if (componentBinder != null) {
            componentBinder.write(component, value);
        }
    }

    public void fillFields(Field[] fields, Object bean) {
        for (Field field : fields) {
        	if (field.getName() != null){
	            JComponent component = field.getEditor();
	            if (component != null) {
	                Object value = null;
	                try {
	                    if (bean != null) {
	                        value = PropertyUtils.getProperty(bean, field.getName());
	                    }
	                } catch (Exception ex) {
	                    throw new RuntimeException(String.format("Unable to recover property '%s'", field.getName()), ex);
	                }
	                fillField(component, value);
	            }
        	}
        }
    }

    public void fillFields(Field[] fields, Map<String, Object> properties) {
        for (String key : properties.keySet()) {
            for (Field field : fields) {
                if (field.getName().equals(key)) {
                    JComponent component = field.getEditor();
                    if (component != null) {
                        Object value = properties.get(key);
                        fillField(component, value);
                    }
                }
            }
        }
    }

    public <T extends JComponent> void fillField(T component, Object value) {
        ComponentBinder<T> componentBinder = (ComponentBinder<T>) getComponentBinder(component.getClass());
        if (componentBinder != null) {
            componentBinder.write(component, value);
        }
    }

    public void fillBean(Field[] fields, Object bean) {
        for (Field field : fields) {
            JComponent component = field.getEditor();
            if (component != null && field.getName() != null) {
                Object value = readComponent(component);
                try {
                    PropertyUtils.setProperty(bean, field.getName(), value);
                } catch (Exception ex) {
                    throw new RuntimeException(String.format("Unable to update property '%s'", field.getName()), ex);
                }
            }
        }
    }

    public void fillBean(Map<String, Object> properties, Object bean) {
        for (String key : properties.keySet()) {
            Object value = properties.get(key);
            try {
                PropertyUtils.setProperty(bean, key, value);
            } catch (Exception ex) {
                throw new RuntimeException(String.format("Unable to update property '%s'", key), ex);
            }
        }
    }

    public <T extends JComponent> Object readComponent(T component) {
        ComponentBinder<T> componentBinder = (ComponentBinder<T>) getComponentBinder(component.getClass());
        if (componentBinder != null) {
            return componentBinder.read(component);
        }
        return null;
    }

    public void fillMap(Field[] fields, Map<String, Object> map) {
        for (Field field : fields) {
            JComponent component = field.getEditor();
            if (component != null) {
                Object value = readComponent(component);
                map.put(field.getName(), value);
            }
        }
    }

    public <T> Map<String, Set<ConstraintViolation<T>>> validate(Class<T> entityClass, Field[] fields) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        javax.validation.Validator validator = factory.getValidator();
        Map<String, Set<ConstraintViolation<T>>> result = new LinkedHashMap<String, Set<ConstraintViolation<T>>>();
        for (Field field : fields) {
        	if (field.getName()!= null){
		        Set<ConstraintViolation<T>> constraintViolations = validator.validateValue(entityClass, field.getName(), getComponentValue(field.getEditor()));
		        if (!constraintViolations.isEmpty()) {
		            result.put(field.getName(), constraintViolations);
		        }
        	}
        }
        return result;
    }
}
